﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Threading;
using System.IO;
using System.Diagnostics;
using System.Reflection;

namespace Jp.Nuits.ForwardProxy
{
    public class ForwardProxy : IDisposable
    {
        #region Const
        /// <summary>
        /// ストリームを読み書きする際のバッファーサイズ
        /// </summary>
        private const int BufferSize = 8 * 1024;
        #endregion

        #region Attribute
        /// <summary>
        /// HttpListener
        /// </summary>
        private HttpListener httpListener = null;
        #endregion

        #region Property
        /// <summary>
        /// プロキシーのリスニングポート
        /// </summary>
        public int ListeningPort { get; set; }
        /// <summary>
        /// Proxyホスト名
        /// </summary>
        public string RemoteHostName { get; set; }
        /// <summary>
        /// プロキシーポート番号
        /// </summary>
        public int RemotePort { get; set; }
        /// <summary>
        /// 認証が必要か否か
        /// </summary>
        public bool IsAuth { get; set; }
        /// <summary>
        /// 認証に必要なユーザー名
        /// </summary>
        public string UserName { get; set; }
        /// <summary>
        /// 認証時のパスワード
        /// </summary>
        public string PassWord { get; set; }
        /// <summary>
        /// プロキシーが実行中か否か
        /// </summary>
        public bool IsRunning
        {
            get { return httpListener != null; }
        }
        #endregion

        #region Constractor
        /// <summary>
        /// インスタンスを生成する。
        /// </summary>
        public ForwardProxy()
        {
        }
        #endregion

        #region Method
        /// <summary>
        /// 実行開始
        /// </summary>
        public void Run()
        {
            lock (this)
            {
                if (httpListener == null)
                {
                    httpListener = new HttpListener();
                    string prefix = string.Format("http://{0}:{1}/", IPAddress.Loopback, ListeningPort);
                    httpListener.Prefixes.Add(prefix);

                    httpListener.Start();
                    httpListener.BeginGetContext(OnGetContext, httpListener);
                }
            }
        }

        /// <summary>
        /// HTTPリクエストの受付処理
        /// </summary>
        private void OnGetContext(IAsyncResult ar)
        {
            lock (this)
            {
                try
                {
                    HttpListenerContext context = ((HttpListener)ar.AsyncState).EndGetContext(ar);

                    //
                    // 受信したリクエストをスレッドプールに渡して送受信処理を行う。
                    //
                    WaitCallback waitCallBack = new WaitCallback(OnRequest);
                    ThreadPool.QueueUserWorkItem(waitCallBack, context);
                }
                catch (Exception e)
                {
                    //
                    // 例外が発生した場合は、ログ出力を行う、再受信処理を続行する。
                    // 
                    Logger.Warning(e);
                }
                finally
                {
                    if (httpListener != null)
                    {
                        httpListener.BeginGetContext(OnGetContext, httpListener);
                    }
                }
            }
        }

        private void OnRequest(object state)
        {
            HttpListenerContext context = state as HttpListenerContext;
            HttpListenerResponse responce = context.Response;
            try
            {
                // Proxyへ送信するリクエストを生成する。
                HttpWebRequest webRequest = CreateRequest(context);

                // リクエストを送信し、レスポンスを受け取る
                using (HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse)
                {
                    // Proxyからのレスポンスをクライアントのレスポンスへ書き出す
                    WriteResponce(responce, webResponse);
                }
            }
            catch (WebException we)
            {
                HttpWebResponse errorResponce = we.Response as HttpWebResponse;
                WriteResponce(responce, errorResponce);
            }
            catch (Exception e)
            {
                Logger.Warning(e);
                // Proxyからエラーが帰ってきた場合の処理。
                // とりあえず適当に実装
                responce = context.Response;
                responce.ProtocolVersion = HttpVersion.Version11;
                responce.StatusCode = 503;
                responce.StatusDescription = "Response Error";
                responce.ContentLength64 = 0;
            }
            finally
            {
                if (responce != null)
                {
                    try
                    {
                        responce.Close();
                    }
                    catch
                    {
                        // ignore
                    }
                }
            }

        }

        private HttpWebRequest CreateRequest(HttpListenerContext context)
        {

            HttpWebRequest webRequest = WebRequest.Create(context.Request.RawUrl) as HttpWebRequest;

            //
            // 親プロキシへの接続情報を設定する。
            //
            webRequest.Proxy = new WebProxy(RemoteHostName, RemotePort);
            if (IsAuth)
            {
                webRequest.Proxy.Credentials = new NetworkCredential(UserName, PassWord);
            }

            //
            // ヘッダー関連情報を設定する。（全て設定しているわけではなく、現状は最低限）
            //
            webRequest.Method = context.Request.HttpMethod;
            webRequest.ProtocolVersion = context.Request.ProtocolVersion;
            webRequest.KeepAlive = context.Request.KeepAlive;
            if (context.Request.UrlReferrer != null)
            {
                webRequest.Referer = context.Request.UrlReferrer.OriginalString;
            }
            webRequest.UserAgent = context.Request.UserAgent;
            webRequest.CookieContainer = new CookieContainer();
            webRequest.CookieContainer.Add(webRequest.RequestUri, context.Request.Cookies);

            //
            // ボディ情報を書き出す。（ただし受信したリクエストにボディが存在した場合）
            //
            if (context.Request.HasEntityBody)
            {
                webRequest.ContentType = context.Request.ContentType;
                if (0 <= context.Request.ContentLength64)
                {
                    webRequest.ContentLength = context.Request.ContentLength64;
                }
                // 現状、transfer-encoding: chunked のことは考えていない
                Stream inputStream = null;
                Stream outputStream = null;
                try
                {
                    inputStream = context.Request.InputStream;
                    outputStream = webRequest.GetRequestStream();

                    Utility.CopyContent(inputStream, outputStream);
                }
                finally
                {
                    Utility.CloseQuiet(inputStream);
                    Utility.CloseQuiet(outputStream);
                }
            }

            return webRequest;
        }

        private static void WriteResponce(HttpListenerResponse responce, HttpWebResponse webResponse)
        {
            //
            // ヘッダー関連情報を設定する。（全て設定しているわけではなく、現状は最低限）
            // 
            responce.ProtocolVersion = webResponse.ProtocolVersion;
            responce.StatusCode = (int)webResponse.StatusCode;
            responce.StatusDescription = webResponse.StatusDescription;

            responce.AddHeader("Server", webResponse.Server);
            responce.ContentType = webResponse.ContentType;
            if (webResponse.ContentLength >= 0)
            {
                responce.ContentLength64 = webResponse.ContentLength;
            }
            if (webResponse.GetResponseHeader("Transfer-Encoding").ToLower().Equals("chunked"))
            {
                responce.SendChunked = true;
            }
            responce.KeepAlive = responce.KeepAlive;

            // ボディの送受信
            Stream inputStream = null;
            Stream outputStream = null;
            try
            {
                inputStream = webResponse.GetResponseStream();
                outputStream = responce.OutputStream;

                Utility.CopyContent(inputStream, outputStream);
            }
            catch
            {
                //
                // クライアントへの書き出し中にエラーが発生した場合は何もできない為そのまま終了する
                // 
            }
            finally
            {
                Utility.CloseQuiet(inputStream);
                Utility.CloseQuiet(outputStream);
            }
        }


        /// <summary>
        /// 実行停止
        /// </summary>
        public void Stop()
        {
            lock (this)
            {
                if (httpListener != null)
                {
                    httpListener.Stop();
                    httpListener = null;
                }
            }
        }
        /// <summary>
        /// リソースを開放する。
        /// </summary>
        public void Dispose()
        {
            Stop();
        }
        #endregion
    }
}
